Skip to content

Add per-call OAuth scope checks - #3128

Merged
SamMorrowDrums merged 4 commits into
mainfrom
sammorrowdrums-scope-policy-model
Aug 25, 2026
Merged

Add per-call OAuth scope checks#3128
SamMorrowDrums merged 4 commits into
mainfrom
sammorrowdrums-scope-policy-model

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Give each tool two straightforward scope checks:

  • a visibility check for classic PAT filtering;
  • a per-call check that returns the exact scopes for an OAuth challenge, or no scopes when the call can continue.

Static tools use small helpers such as RequireAll. Conditional tools use ordinary Go functions over the tool arguments and active token scopes.

Why

Scope requirements can depend on the call. Listing issue fields uses repo for a repository request and read:org for an organization request. File writes need workflow in addition to repo only when changing a workflow file.

Keeping visibility and per-call challenges separate avoids flattening these cases into one scope list or introducing a policy language.

What changed

  • Added ScopeAccess with Visible and Challenge functions.
  • Migrated every tool to explicit RequireAll, PublicRead, NoScopes, or a small custom check.
  • Made PAT filtering call Visible directly.
  • Made OAuth middleware forward the exact scopes returned by Challenge.
  • Added argument-aware checks for issue fields, issue types, UI data, and workflow-file writes.
  • Declared an exhaustive maximum scope set for every dynamic challenge.
  • Kept MCP tool arguments as json.RawMessage until a call-specific decision needs them.
  • Added a max-satisfied fast path that uses normalized scope hierarchy checks and skips argument materialization plus the dynamic callback.
  • Updated list-scopes and generated tool docs to show every scope a tool may challenge for.
  • Kept file paths in the parsed MCP body rather than projecting them into headers.

Performance

A local benchmark (not committed) compared the max-satisfied path with the call-specific decode path while both continued through downstream argument parsing:

  • small arguments: about 21% lower latency, 29 fewer allocations, and about 1.5 KiB less allocated per call;
  • 32 KiB arguments: about 23% lower latency, 29 fewer allocations, and about 34 KiB less allocated per call.

The implementation change is net +82 production lines; most of the remaining diff is regression coverage.

Related work

Supersedes #2778 and #3092.

Copilot AI balanced review requested due to automatic review settings August 20, 2026 15:20
@SamMorrowDrums
SamMorrowDrums requested a review from a team as a code owner August 20, 2026 15:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces explicit OAuth authorization policies with alternative paths, conjunctive requirements, and argument-aware scope resolution.

Changes:

  • Adds policy modeling, evaluation, hierarchy traversal, and compatibility projection.
  • Resolves scopes dynamically for repository/org operations and workflow-file writes.
  • Adds path validation, middleware integration, documentation, and tests.
Show a summary per file
File Description
pkg/scopes/scopes.go Implements policy construction and evaluation.
pkg/scopes/scopes_test.go Tests policy semantics and hierarchy expansion.
pkg/scopes/map.go Maps and resolves tool scope policies.
pkg/scopes/map_test.go Tests call-specific policy resolution.
pkg/inventory/server_tool.go Adds policy metadata to tools.
pkg/inventory/server_tool_test.go Verifies path headers are excluded.
pkg/http/middleware/scope_challenge.go Applies resolved policies to OAuth challenges.
pkg/http/middleware/scope_challenge_test.go Tests argument-aware challenges.
pkg/github/ui_tools.go Adds a scope resolver to ui_get.
pkg/github/tool_scopes.go Defines repository/org and UI policies.
pkg/github/tool_scopes_test.go Tests conditional tool policies.
pkg/github/scope_filter.go Filters tools using authorization paths.
pkg/github/scope_filter_test.go Tests compatibility and fail-open behavior.
pkg/github/repository_path.go Validates paths and detects workflow writes.
pkg/github/repository_path_test.go Tests path safety and workflow policies.
pkg/github/repositories.go Applies validation and dynamic write policies.
pkg/github/issues.go Adds issue-type scope resolution.
pkg/github/issue_fields.go Adds issue-field scope resolution.
pkg/github/header_params_test.go Ensures paths remain in MCP arguments.
pkg/github/dependencies.go Initializes legacy-compatible policies.
docs/scope-filtering.md Documents the policy model.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 21/21 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread pkg/github/tool_scopes.go Outdated
Comment thread pkg/github/repository_path.go Outdated
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-scope-policy-model branch from 442d43f to 96f14fd Compare August 21, 2026 00:16
@SamMorrowDrums SamMorrowDrums changed the title Model alternative OAuth scope policies Model OAuth scope policies Aug 21, 2026
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-scope-policy-model branch 2 times, most recently from 05d9ff1 to 1cadd8a Compare August 24, 2026 09:57
@SamMorrowDrums SamMorrowDrums changed the title Model OAuth scope policies Add per-call OAuth scope checks Aug 24, 2026
Let each tool define fixed-token visibility and return the exact scopes for an
OAuth challenge based on the current call.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 26e41558-43f9-42b2-8569-8489957c2b0a
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-scope-policy-model branch from 1cadd8a to 1850911 Compare August 24, 2026 12:14
Retain raw MCP arguments until a call-specific scope decision is needed. Tokens that satisfy a tool's exhaustive maximum scopes now bypass argument materialization and dynamic challenge callbacks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove the temporary performance benchmark from the committed test suite while retaining its measurements in the pull request description.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 54/54 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread pkg/github/tool_scopes.go
Defer OAuth challenges to normal handler validation when ui_get reviewer calls omit or malform the repository argument.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@SamMorrowDrums
SamMorrowDrums merged commit f0baf1c into main Aug 25, 2026
20 checks passed
@SamMorrowDrums
SamMorrowDrums deleted the sammorrowdrums-scope-policy-model branch August 25, 2026 13:52
SamMorrowDrums added a commit that referenced this pull request Aug 25, 2026
…allenges

Rebase onto main (which now includes #3128's per-call OAuth scope checks)
and redesign the ruleset tools around that API instead of the old
tool-name-keyed challenge middleware that motivated splitting by level.

- Collapse the 5 level-specific tools (repository_ruleset_read,
  organization_repository_ruleset_read, create_repository_ruleset,
  create_organization_repository_ruleset, create_enterprise_repository_ruleset)
  into 2: repository_ruleset_read and create_repository_ruleset. Both take a
  "level" argument (repository/organization/enterprise) and use
  scopes.DynamicChallenge to return the exact repo / read:org / admin:org /
  read:enterprise / admin:enterprise challenge for that call, using the scope
  hierarchy so a broader granted scope still satisfies the challenge.
- A missing or unrecognized "level" (or a non-string value) returns no
  challenge so normal handler argument validation produces the error,
  instead of prompting for scopes on a malformed call.
- Keep repository_ruleset_read and create_repository_ruleset as separate
  tools since ReadOnlyHint-based read-only filtering depends on that split.
- Add enterprise-level "get" and "list" read support (list issued directly
  via GET /enterprises/{enterprise}/rulesets, matching the existing rule
  suite pattern, since go-github has no typed wrapper for it) so all three
  levels have symmetric read coverage.
- Re-add read:enterprise/admin:enterprise to pkg/scopes as opt-in
  (non-default) OAuth scopes, alongside admin:org which no tool had
  previously requested, and register them in oauthScopeDefinitions so they
  are advertised in OAuth protected-resource metadata.
- Fetch the "law" toolset icon into the new icons_data_uris.txt embed
  (required_icons.txt already listed it) and bump go-github v87 -> v89 to
  match main.
- Update README/toolsnaps via script/generate-docs and UPDATE_TOOLSNAPS.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants